package org.paradigmshift.cumulusfracto.filesystem;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.regex.Pattern;
import org.alfresco.config.ConfigElement;
import org.alfresco.jlan.server.SrvSession;
import org.alfresco.jlan.server.core.DeviceContext;
import org.alfresco.jlan.server.core.DeviceContextException;
import org.alfresco.jlan.server.filesys.AccessDeniedException;
import org.alfresco.jlan.server.filesys.DiskInterface;
import org.alfresco.jlan.server.filesys.DiskOfflineException;
import org.alfresco.jlan.server.filesys.FileAttribute;
import org.alfresco.jlan.server.filesys.FileInfo;
import org.alfresco.jlan.server.filesys.FileOpenParams;
import org.alfresco.jlan.server.filesys.FileStatus;
import org.alfresco.jlan.server.filesys.NetworkFile;
import org.alfresco.jlan.server.filesys.PathNotFoundException;
import org.alfresco.jlan.server.filesys.SearchContext;
import org.alfresco.jlan.server.filesys.TreeConnection;
import org.apache.log4j.Logger;
import com.google.gdata.client.GoogleService.CaptchaRequiredException;
import com.google.gdata.client.GoogleService.ServiceUnavailableException;
import com.google.gdata.client.docs.DocsService;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.MediaContent;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.docs.DocumentListEntry;
import com.google.gdata.data.docs.DocumentListFeed;
import com.google.gdata.data.docs.FileEntry;
import com.google.gdata.data.docs.FolderEntry;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ResourceNotFoundException;
import com.google.gdata.util.ServiceException;

public class CumulusFractoDiskDrive implements DiskInterface {

	private static final Logger log = Logger.getLogger( CumulusFractoDiskDrive.class );
	
	private static final String DEVICE = "http://docs.google.com/feeds/default/private/full";
	private static final String ROOT_FOLDER = "/folder%3Aroot";
	private static final String CONTENTS = "/contents";
	private static final String FOLDER = "/folder";
	private static final String HIDDEN = "/hidden";
	private static final String DOCS_SERVICE_UNREACHABLE = "An error occured while trying to access the Google Docs service.";
	
	private static final String KEY_ROOT_PATH = "rootPath";
	private static final String KEY_DEVICE_NAME = "deviceName";
	
	private static final Long DATE = System.currentTimeMillis();
	private static final Pattern SEARCH_PATTERN = Pattern.compile( "^(\\\\(?:.+\\\\)*)(.*)$" );
	private static final String PATH_SEPARATOR = "\\";
	private static final String PATH_SEPARATOR_REGEX = "\\\\";
	
	/**
	 * Class constructor
	 * @throws ServiceException 
	 * @throws IOException 
	 */
	public CumulusFractoDiskDrive() {}
	
	/**
	 * Close the specified file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param networkFile Network file details
	 * @exception IOException
	 */
	@Override
	public void closeFile( SrvSession session, TreeConnection tree, NetworkFile networkFile ) throws IOException {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "closeFile [ file=" );
			debugMessage.append( networkFile.getName() );
			debugMessage.append( " ]" );
			log.debug(  debugMessage.toString() );
		}
		
		// Close the file
		networkFile.closeFile();

		// Check if the file/directory is marked for delete
		if ( networkFile.hasDeleteOnClose() ) {

			// Check for a file or directory
			if ( networkFile.isDirectory() ) {
				
				deleteDirectory( session, tree, networkFile.getFullName() );
			}
			else {
				
				deleteFile( session, tree, networkFile.getFullName() );
			}
		}
	}

	/**
	 * Create a new directory
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param params Directory parameters
	 * @exception IOException
	 */
	@Override
	public void createDirectory( SrvSession session, TreeConnection tree, FileOpenParams params ) throws IOException {
		
		CumulusFractoClientInfo clientInfo = (CumulusFractoClientInfo) session.getClientInformation();
		String rawPath = params.getPath();
		
		if (log.isDebugEnabled()) {
			
			StringBuilder debugMessage = new StringBuilder( "createDirectory [ path=" );
			debugMessage.append( rawPath );
			debugMessage.append( " ]" );
			log.debug(  debugMessage.toString() );
		}
		
		// Get the parent path and filename
		String parentPath = this.getParentPath( rawPath );
		String directoryName = this.getFilename( rawPath );
		
		// Get parent directory entry
		DocumentListEntry parentEntry = this.getEntry( clientInfo.getDocsService(), clientInfo.getRootContentsFeed(), parentPath );
		
		// Create the folder
		FolderEntry folder = new FolderEntry();
		folder.setTitle( new PlainTextConstruct( directoryName ) );
		URL parentUrl = new URL( ((MediaContent) parentEntry.getContent()).getUri() );
		this.insertEntry( clientInfo.getDocsService(), parentUrl, folder );
	}

	/**
	 * Create a new file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param params File open parameters
	 * @return NetworkFile
	 * @exception IOException
	 */
	@Override
	public NetworkFile createFile( SrvSession session, TreeConnection tree, FileOpenParams params ) throws IOException {
		
		CumulusFractoClientInfo clientInfo = (CumulusFractoClientInfo) session.getClientInformation();
		String rawPath = params.getPath();
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "createFile [ filePath=" );
			debugMessage.append( rawPath );
			debugMessage.append( " ]" );
			log.debug(  debugMessage.toString() );
		}
		
		// Get the parent path and filename
		String parentPath = this.getParentPath( rawPath );
		String filename = this.getFilename( rawPath );
		
		// Create new file entry
		DocumentListEntry newEntry = new FileEntry();
		newEntry.setTitle( new PlainTextConstruct( filename ) );
		
		// Insert into google docs
		DocsService docsService = clientInfo.getDocsService();
		DocumentListEntry parentEntry = this.getEntry( docsService, clientInfo.getRootContentsFeed(), parentPath );
		URL parentUrl = new URL( ((MediaContent) parentEntry.getContent()).getUri() );
		newEntry = this.insertEntry( docsService, parentUrl, newEntry );
		
		// Create and return the network file
		return CumulusFractoNetworkFile.createFile( newEntry, params );
	}

	/**
	 * Delete a directory
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param directoryPath Path of directory to delete
	 * @exception IOException
	 */
	@Override
	public void deleteDirectory( SrvSession session, TreeConnection tree, String directoryPath ) throws IOException {
		
		CumulusFractoClientInfo clientInfo = (CumulusFractoClientInfo) session.getClientInformation();
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder();
			debugMessage.append( "deleteDirectory [ directoryPath=" );
			debugMessage.append( directoryPath );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		DocumentListEntry entryToDelete = this.getEntry( clientInfo.getDocsService(), clientInfo.getRootContentsFeed(), directoryPath );
		this.deleteEntry( entryToDelete );
	}

	/**
	 * Delete a file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param filePath Name of file to delete
	 * @exception IOException
	 */
	@Override
	public void deleteFile( SrvSession session, TreeConnection tree, String filePath ) throws IOException {
		
		CumulusFractoClientInfo clientInfo = (CumulusFractoClientInfo) session.getClientInformation();
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "deleteFile [ filePath=" );
			debugMessage.append( filePath );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		DocsService docsService = clientInfo.getDocsService();
		DocumentListEntry entryToDelete = this.getEntry( docsService, clientInfo.getRootContentsFeed(), filePath );
		this.deleteEntry( entryToDelete );
	}

	/**
	 * Check if the specified file exists, and it is a file.
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param filePath File name
	 * @return int
	 */
	@Override
	public int fileExists( SrvSession session, TreeConnection tree, String filePath ) {
		
		CumulusFractoClientInfo clientInfo = (CumulusFractoClientInfo) session.getClientInformation();
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "fileExists [ filePath=" );
			debugMessage.append( filePath );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		int exists = FileStatus.NotExist;
		try {
			
			DocumentListEntry entry = this.getEntry( clientInfo.getDocsService(), clientInfo.getRootContentsFeed(), filePath );
			if ( entry != null ) {
				
				if ( isFolder( entry ) ) {
					
					exists = FileStatus.DirectoryExists;
				}
				else {
					
					exists = FileStatus.FileExists;
				}
			}
		}
		catch ( IOException ioe ) {
			
			// Do nothing - fall through and return false
		}

		return exists;
	}

	/**
	 * Flush buffered data for the specified file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param networkFile Network file
	 * @exception IOException
	 */
	@Override
	public void flushFile( SrvSession session, TreeConnection tree, NetworkFile networkFile ) throws IOException {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "flushFile [ networkFile=" );
			debugMessage.append( networkFile.getName() );
			debugMessage.append( " ]" );
			log.debug(  debugMessage.toString() );
		}
		
		// Flush the file
		networkFile.flushFile();
	}

	/**
	 * Return file information about the specified file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param rawPath File name
	 * @return SMBFileInfo
	 * @exception IOException
	 */
	@Override
	public FileInfo getFileInformation( SrvSession session, TreeConnection tree, String rawPath ) throws IOException {
		
		CumulusFractoClientInfo clientInfo = (CumulusFractoClientInfo) session.getClientInformation();
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "getFileInformation [ filePath=" );
			debugMessage.append( rawPath );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		FileInfo fileInfo = null;
		if ( PATH_SEPARATOR.equals( rawPath ) ) {
			
			// Create the file information
			fileInfo = new FileInfo( rawPath, 0, FileAttribute.Directory );
			fileInfo.setFileId( ROOT_FOLDER.hashCode() );
		}
		else {
			
			// Get entry specified by the full path name
			DocumentListEntry entry = this.getEntry( clientInfo.getDocsService(), clientInfo.getRootContentsFeed(), rawPath );
			
			int fileAttributes = 0;
			if ( isFolder( entry ) ) {
				
				fileAttributes = FileAttribute.Directory;
			}
			
			if ( !entry.getCanEdit() ) {
				
				fileAttributes += FileAttribute.ReadOnly;
			}
			
			// Create the file information
			fileInfo = new FileInfo( entry.getTitle().getPlainText(), entry.getQuotaBytesUsed(), fileAttributes );
			
			Long editDate = entry.getEdited().getValue();
			fileInfo.setModifyDateTime( editDate );
			fileInfo.setChangeDateTime( editDate );
			
			DateTime lastViewed = entry.getLastViewed();
			if ( lastViewed != null ) fileInfo.setAccessDateTime( lastViewed.getValue() );
			
			fileInfo.setCreationDateTime( entry.getPublished().getValue() );
			fileInfo.setFileId( entry.getResourceId().hashCode() );
		}

		return fileInfo;
	}

	/**
	 * Determine if the disk device is read-only.
	 * 
	 * @param session Session details
	 * @param context Device context
	 * @return true if the device is read-only, else false
	 * @exception IOException If an error occurs.
	 */
	@Override
	public boolean isReadOnly( SrvSession session, DeviceContext deviceContext ) throws IOException {
		
		CumulusFractoClientInfo clientInfo = (CumulusFractoClientInfo) session.getClientInformation();
		CumulusFractoContext context = (CumulusFractoContext) deviceContext;
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "isReadOnly [ deviceName=" );
			debugMessage.append( context.getDeviceName() );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		return clientInfo.getRootContentsFeed().isImmutable();
	}

	/**
	 * Open a file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param params File open parameters
	 * @return NetworkFile
	 * @exception IOException
	 */
	@Override
	public NetworkFile openFile( SrvSession session, TreeConnection tree, FileOpenParams params ) throws IOException {
		
		CumulusFractoClientInfo clientInfo = (CumulusFractoClientInfo) session.getClientInformation();
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "openFile [ filePath=" );
			debugMessage.append( params.getPath() );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		// Get the network file
		String rawPath = params.getPath();
		DocumentListEntry entry = this.getEntry( clientInfo.getDocsService(), clientInfo.getRootContentsFeed(), rawPath );
		
		if ( !entry.getCanEdit() && ( params.isReadWriteAccess() || params.isWriteOnlyAccess() ) )
			throw new AccessDeniedException( "File " + rawPath + " is read-only." );
		
		CumulusFractoNetworkFile networkFile = CumulusFractoNetworkFile.createFile( entry, params );
		
		// Check if the file is actually a directory
		if ( isFolder( entry ) ) {
			
			networkFile.setAttributes( FileAttribute.Directory );
		}
		else {
			
			networkFile.setAttributes( FileAttribute.NTNormal );
		}
		
		// Set access type
		if (params.isReadOnlyAccess()) {
			
			networkFile.setGrantedAccess( NetworkFile.READONLY );
		}
		else {
			
			networkFile.setGrantedAccess( NetworkFile.READWRITE );
		}
		
		return networkFile;
	}

	/**
	 * Read a block of data from a file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param networkFile Network file
	 * @param buf Buffer to return data to
	 * @param bufPos Starting position in the return buffer
	 * @param size Maximum size of data to return
	 * @param filePos File offset to read data
	 * @return Number of bytes read
	 * @exception IOException
	 */
	@Override
	public int readFile( SrvSession session, TreeConnection tree, NetworkFile networkFile, byte[] buf, int bufPos, int size, long filePos ) throws IOException {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "readFile [ file=" );
			debugMessage.append( networkFile.getName() );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		if ( networkFile.isDirectory() ) throw new AccessDeniedException();
		
		// Read the file
		int readLength = networkFile.readFile( buf, size, bufPos, filePos );
		if ( readLength == -1 ) {
			
			readLength = 0;
		}
		
		return readLength;
	}

	/**
	 * Rename a file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param oldName Existing file name
	 * @param newName New file name
	 * @exception IOException
	 */
	@Override
	public void renameFile( SrvSession session, TreeConnection tree, String sourcePath, String destinationPath ) throws IOException {
		
		CumulusFractoClientInfo clientInfo = (CumulusFractoClientInfo) session.getClientInformation();
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "renameFile [ source=" );
			debugMessage.append( sourcePath );
			debugMessage.append( ", destination=" );
			debugMessage.append( destinationPath );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		DocsService docsService = clientInfo.getDocsService();
		DocumentListFeed rootFeed = clientInfo.getRootContentsFeed();
		DocumentListEntry sourceEntry = this.getEntry( docsService, rootFeed, sourcePath );
		
		// Rename the file if necessary
		String sourceName = this.getFilename( sourcePath );
		String destinationName = this.getFilename( destinationPath );
		if ( destinationName != null && !destinationName.equals( sourceName ) ) {
			
			sourceEntry.setTitle( new PlainTextConstruct( destinationName ) );
			this.editEntry( sourceEntry );
		}
		
		// Differing directories - must move file
		String sourceParentPath = this.getParentPath( sourcePath );
		String destinationParentPath = this.getParentPath( destinationPath );
		if ( !sourceParentPath.equals( destinationParentPath ) ) {
			
			DocumentListEntry destinationEntry = this.getEntry( docsService, rootFeed, destinationParentPath );
			
			// Add the file to the new directory
			DocumentListEntry newEntry = new DocumentListEntry();
			newEntry.setResourceId( sourceEntry.getResourceId() );
			String destinationFolderUri = ((MediaContent) destinationEntry.getContent()).getUri();
			this.insertEntry( docsService, new URL( destinationFolderUri ), newEntry );
			
			// Delete the reference in the old directory
			DocumentListEntry sourceParentEntry = this.getEntry( docsService, rootFeed, sourceParentPath );
			String sourceFolderUri = ((MediaContent) sourceParentEntry.getContent()).getUri();
			this.deleteEntry( docsService, new URL( sourceFolderUri + "/" + sourceEntry.getResourceId() ), sourceEntry.getEtag() );
		}
	}

	/**
	 * Seek to the specified point within a file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param networkFile Network file
	 * @param position New file position
	 * @param type Seek type
	 * @return New file position
	 * @exception IOException
	 */
	@Override
	public long seekFile( SrvSession session, TreeConnection tree, NetworkFile networkFile, long position, int type ) throws IOException {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "seekFile [ name=" );
			debugMessage.append( networkFile.getName() );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		// Check that the network file is our type
		return networkFile.seekFile( position, type );
	}

	/**
	 * Set file information
	 * 
	 * @param sesssion Session details
	 * @param tree Tree connection
	 * @param filePath File name
	 * @param info File information to be set
	 * @exception IOException
	 */
	@Override
	public void setFileInformation( SrvSession sesssion, TreeConnection tree, String filePath, FileInfo info ) throws IOException {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "setFileInformation [ path=" );
			debugMessage.append( filePath );
			debugMessage.append( " ]" );
			
			log.debug( debugMessage.toString() );
		}
		
		// Google handles the setting of file info - client doesn't/shouldn't do this
	}

	/**
	 * Start a file search
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param searchPath Search path, may include wildcards
	 * @param attributes Search attributes
	 * @return SearchContext
	 * @exception FileNotFoundException
	 */
	@Override
	public SearchContext startSearch( SrvSession session, TreeConnection tree, String searchPath, int attributes ) throws FileNotFoundException {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "startSearch [ searchPath=" );
			debugMessage.append( searchPath );
			debugMessage.append( ", attributes=" );
			debugMessage.append( attributes );
			debugMessage.append( " ]" );
			
			log.debug( debugMessage.toString() );
		}
		
		// Parse search term - separate search string from directory structure
		String searchTerm = searchPath.substring( searchPath.lastIndexOf( PATH_SEPARATOR ) );
		
		// Create a context for the new search
		return new CumulusFractoSearchContext( session, tree, searchTerm );
	}

	/**
	 * Truncate a file to the specified size
	 * 
	 * @param session Server session
	 * @param tree Tree connection
	 * @param networkFile Network file details
	 * @param size New file length
	 * @exception java.io.IOException The exception description.
	 */
	@Override
	public void truncateFile( SrvSession session, TreeConnection tree, NetworkFile networkFile, long size ) throws IOException {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "truncateFile [ name=" );
			debugMessage.append( networkFile.getName() );
			debugMessage.append( ", size=" );
			debugMessage.append( size );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		networkFile.truncateFile( size );
		networkFile.flushFile();
	}

	/**
	 * Write a block of data to a file
	 * 
	 * @param session Session details
	 * @param tree Tree connection
	 * @param networkFile Network file
	 * @param buffer Data to be written
	 * @param bufferOffset Offset of data within the buffer
	 * @param size Number of bytes to be written
	 * @param fileOffset Offset within the file to start writing the data
	 */
	@Override
	public int writeFile( SrvSession session, TreeConnection tree, NetworkFile networkFile, byte[] buffer, int bufferOffset, int size, long fileOffset ) throws java.io.IOException {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "writeFile [ name=" );
			debugMessage.append( networkFile.getName() );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
		
		if ( networkFile.isDirectory() ) {
			
			throw new AccessDeniedException();
		}
		
		// Write the data to the file
		networkFile.writeFile( buffer, size, bufferOffset, fileOffset );
		return size;
	}

	/**
	 * Parse and validate the parameter string and create a device context for
	 * this share
	 * 
	 * @param shareName String
	 * @param configElement ConfigElement
	 * @return DeviceContext
	 * @exception DeviceContextException
	 */
	@Override
	public DeviceContext createContext( String shareName, ConfigElement configElement ) throws DeviceContextException {
		
		if ( log.isDebugEnabled() ) log.debug( "createContext [ shareName=" + shareName + " ]"  );
		
		// Create the device context
		return new CumulusFractoContext( shareName, "" );
	}
	

	/**
	 * Connection opened to this disk device
	 * 
	 * @param session Server session
	 * @param tree Tree connection
	 */
	@Override
	public void treeOpened( SrvSession session, TreeConnection tree ) {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "treeOpened [ deviceName=" );
			debugMessage.append( tree.getContext().getDeviceName() );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
	}

	/**
	 * Connection closed to this device
	 * 
	 * @param session Server session
	 * @param tree Tree connection
	 */
	@Override
	public void treeClosed( SrvSession session, TreeConnection tree ) {
		
		if ( log.isDebugEnabled() ) {
			
			StringBuilder debugMessage = new StringBuilder( "treeClosed [ deviceName=" );
			debugMessage.append( tree.getContext().getDeviceName() );
			debugMessage.append( " ]" );
			log.debug( debugMessage.toString() );
		}
	}
	
	
	/*******************************************
	 *  HELPER FUNCTIONS
	 *******************************************/
	public static boolean isFolder( DocumentListEntry entry ) {
		
		String resourceId = entry.getResourceId();
		String type = resourceId.substring( 0, resourceId.lastIndexOf( ":" ) );
		return "folder".equals( type );
	}
	
	private String getParentPath( String path ) {
		
		String parentPath = path.substring( 0, path.lastIndexOf( PATH_SEPARATOR ) + 1 );
		if ( parentPath == null || "".equals( parentPath ) ) parentPath = PATH_SEPARATOR; 
		return parentPath;
	}
	
	private String getFilename( String path ) {
		
		return path.substring( path.lastIndexOf( PATH_SEPARATOR ) + 1 );
	}
	
	private DocumentListEntry insertEntry( DocsService docsService, URL parentUrl, DocumentListEntry entry ) throws IOException {
		
		try {
			
			// Check if already exists
//			String title = entry.getTitle().getPlainText();
//			DocumentQuery query = new DocumentQuery( parent );
//			query.setTitleExact( true );
//			query.setTitleQuery( title );
//			
//			DocumentListFeed testFeed = docsService.getFeed( query, DocumentListFeed.class );
//			if ( testFeed.getEntries().size() > 0 ) throw new FileExistsException();
			
			return docsService.insert( parentUrl, entry );
		}
		catch ( CaptchaRequiredException cre ) {
			
			// TODO: send user to the captcha web page
			throw new AccessDeniedException( "Captcha required." );
		}
		catch ( ServiceUnavailableException sue ) {
			
			throw new DiskOfflineException( "Google Docs unavailable." );
		}
		catch ( AuthenticationException ae ) {
			
			throw new AccessDeniedException( "Access denied." );
		}
		catch ( ResourceNotFoundException rnfe ) {
			
			throw new FileNotFoundException( "Resource not found." );
		}
		catch ( ServiceException se ) {
			
			throw new IOException( se );
		}
	}
	
	private DocumentListEntry findEntryInFeed( DocumentListFeed feed, String entryName ) throws IOException {
		
		List<DocumentListEntry> entries = feed.getEntries();
		for ( DocumentListEntry entry : entries ) {
			
			if ( entry.getTitle().getPlainText().equals( entryName ) ) {
				
				return entry;
			}
		}
		
		throw new FileNotFoundException( entryName );
	}
	
	private DocumentListEntry getEntry( DocsService docsService, DocumentListFeed rootFeed, String path ) throws IOException {
		
		if ( rootFeed == null ) throw new NullPointerException( "Feed is null." );
		if ( path == null ) throw new NullPointerException( "Path is null." );
		
		DocumentListFeed feed = rootFeed;
		DocumentListEntry entry = null;
		
		String[] splitPath = path.split( PATH_SEPARATOR_REGEX );
		
		for ( int i = 1; i < splitPath.length; i++ ) {
			
			String currentLevel = splitPath[ i ];
			entry = findEntryInFeed( feed, currentLevel );
			
			if ( i < (splitPath.length - 1) && isFolder( entry ) ) {
				
				String folderUriString = ((MediaContent) entry.getContent()).getUri();
				try {
					
					feed = docsService.getFeed( new URL( folderUriString ), DocumentListFeed.class );
				}
				catch ( CaptchaRequiredException cre ) {
					
					// TODO: send user to the captcha web page
					throw new AccessDeniedException( "Captcha required." );
				}
				catch ( ServiceUnavailableException sue ) {
					
					throw new DiskOfflineException( "Google Docs unavailable." );
				}
				catch ( AuthenticationException ae ) {
					
					throw new AccessDeniedException( "Access denied." );
				}
				catch ( ResourceNotFoundException rnfe ) {
					
					throw new FileNotFoundException( "Resource not found: " + currentLevel );
				}
				catch ( ServiceException se ) {
					
					throw new IOException( se );
				}
			}
			else if ( i < (splitPath.length - 1) ) {
				
				// Invalid path - reached a non-folder before we found entry
				throw new PathNotFoundException( path );
			}
		}
		
		return entry;
	}
	
	private void editEntry( DocumentListEntry entry ) throws IOException {
		
		try {
			
			entry.update();
		}
		catch ( CaptchaRequiredException cre ) {
			
			// TODO: send user to the captcha web page
			throw new AccessDeniedException( "Captcha required." );
		}
		catch ( ServiceUnavailableException sue ) {
			
			throw new DiskOfflineException( "Google Docs unavailable." );
		}
		catch ( AuthenticationException ae ) {
			
			throw new AccessDeniedException( "Access denied." );
		}
		catch ( ResourceNotFoundException rnfe ) {
			
			throw new FileNotFoundException( "Resource not found." );
		}
		catch ( ServiceException se ) {
			
			throw new IOException( se );
		}
	}
	
	private void deleteEntry( DocumentListEntry entry ) throws IOException {
		
		try {
			
			entry.delete();
		}
		catch ( CaptchaRequiredException cre ) {
			
			// TODO: send user to the captcha web page
			throw new AccessDeniedException( "Captcha required." );
		}
		catch ( ServiceUnavailableException sue ) {
			
			throw new DiskOfflineException( "Google Docs unavailable." );
		}
		catch ( AuthenticationException ae ) {
			
			throw new AccessDeniedException( "Access denied." );
		}
		catch ( ResourceNotFoundException rnfe ) {
			
			throw new FileNotFoundException( "Resource not found: " + entry.getTitle().getPlainText() );
		}
		catch ( ServiceException se ) {
			
			throw new IOException( se );
		}
	}
	
	private void deleteEntry( DocsService docsService, URL url, String etag ) throws IOException {
		
		try {
			
			docsService.delete( url, etag );
		}
		catch ( CaptchaRequiredException cre ) {
			
			// TODO: send user to the captcha web page
			throw new AccessDeniedException( "Captcha required." );
		}
		catch ( ServiceUnavailableException sue ) {
			
			throw new DiskOfflineException( "Google Docs unavailable." );
		}
		catch ( AuthenticationException ae ) {
			
			throw new AccessDeniedException( "Access denied." );
		}
		catch ( ResourceNotFoundException rnfe ) {
			
			throw new FileNotFoundException( "Resource not found. " );
		}
		catch ( ServiceException se ) {
			
			throw new IOException( se );
		}
	}
	
//	private DocumentListEntry uploadToGoogleDocs( String directoryPath, DocumentListEntry entry ) throws ServiceException, IOException {
//		
//		DocumentListEntry uploadedEntry = null;
//		
//		// Always dealing with absolute paths, so if directoryPath is null it means the path has no parent (ie is root)
//		if ( directoryPath != null ) {
//			
//			if ( isRoot( directoryPath ) ) {
//				
//				StringBuilder rootSb = new StringBuilder( DEVICE );
//				rootSb.append( ROOT_FOLDER );
//				rootSb.append( CONTENTS );
//				
//				URL rootUrl = new URL( rootSb.toString() );
//				DocumentListFeed rootFeed = docsService.getFeed( rootUrl, DocumentListFeed.class );
//				
//				uploadedEntry = rootFeed.insert( entry );
//			}
//			else {
//				
//				DocumentListEntry folderEntry = getEntryByAbsolutePath( directoryPath );
//				uploadedEntry = docsService.insert( new URL( ((MediaContent) folderEntry.getContent()).getUri() ), entry );
//			}
//		}
//		
//		return uploadedEntry;
//	}
}
